/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import static android.persist.op.sqlite.SQLLogging.SQL_LOG_TAG;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import android.persist.PersistException;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;
import android.persist.op.OpException;
import android.persist.op.Read;
import android.util.Log;

/**
 * Populates an objects foreign reference fields with data read in from the
 * underlying data store.
 * 
 * @param <OT> Operand Type that can be passed to the constructor.
 */
public class SQLiteReadForeign<OT> extends Read<OT, List<Object>> {

    /**
     * The database that this operation is applied against.
     * 
     * @since 1.0
     */
    private final SQLiteDatabase mDatabase;

    /**
     * Class configuration object for {@link operand}'s type.
     * 
     * @since 1.0
     */
    private final ClassConfig<OT> mConfig;

    /**
     * Creates a new read operation to read in a class from the database.
     * 
     * @param db the database to read from.
     * @param c the class type to read.
     * @param where if not-null then this will be used directly as the WHERE
     *            clause in the SQL statement.
     * @throws IllegalArgumentException if db or o are null.
     * @since 1.0
     */
    @SuppressWarnings({
            "unchecked", "rawtypes"
    })
    public SQLiteReadForeign(SQLiteDatabase db, OT o) {
        super(o, null);

        if (db == null) {
            throw new IllegalArgumentException("db cannot be null");
        }

        if (o == null) {
            throw new IllegalArgumentException("o cannot be null");
        }

        this.mDatabase = db;
        this.mConfig = (ClassConfig<OT>) ClassConfig.getConfig(o.getClass());

    }

    /**
     * Reads the foreign field values from the database and populates the object
     * that was passed to the constructor.
     * 
     * @return the list of objects that were read.
     * @since 1.0
     * @throws PersistException if the read fails
     * @throws SQLiteException if the read fails
     */
    @Override
    public List<Object> call() {
        if (!mDatabase.isOpen())
            throw new OpException("database is closed");

        List<Object> result = new ArrayList<Object>();
        /*
         * load each foreign field
         */

        for (FieldConfig fc : mConfig.foreignFields) {

            try {
                Object value;

                if (fc.isCollection()) {

                    value = readCollection(fc);

                } else {

                    value = readNonCollection(fc);

                }
                fc.field.set(operand, value);
                result.add(value);
            } catch (IllegalArgumentException e) {
                throw new PersistException("unable to read foreign key "
                        + fc.getPersistedName()
                        + " for class " + operand.getClass().getSimpleName());
            } catch (IllegalAccessException e) {
                throw new PersistException("unable to read foreign key "
                        + fc.getPersistedName()
                        + " for class " + operand.getClass().getSimpleName());
            }

        }

        return result;

    }

    /**
     * Reads a foreign field that is not a collection and returns it's object.
     * 
     * @param fConfig
     * @return
     */
    private Object readNonCollection(FieldConfig fConfig) {

        Map<String, Object> foreignRowValue = readForeignKeyValue(fConfig);

        // create a class configuration for the foreign type.
        @SuppressWarnings({
                "unchecked", "rawtypes"
        })
        ClassConfig<?> foreignConfig = (ClassConfig<OT>) ClassConfig.getConfig(fConfig.field
                .getType());

        // get the values of the foreign keys from operand's data store.

        @SuppressWarnings({
                "unchecked", "rawtypes"
        })
        SQLiteRead<?> foreignRead = new SQLiteRead(mDatabase, foreignConfig.clazz,
                foreignRowValue);

        List<?> readResult = foreignRead.call();
        if (readResult.size() == 0)
            return null;

        if (readResult.size() > 1)
            throw new PersistException("read foreign read "
                    + Integer.toString(readResult.size()) + " objects (expected 0 or 1)");

        return readResult.get(0);

    }

    private Object readCollection(FieldConfig fConfig) {

        StringBuilder collectionTableName = new StringBuilder();
        collectionTableName.append(mConfig.storageName);
        collectionTableName.append("_");
        collectionTableName.append(fConfig.getPersistedName());

        StringBuilder idQuery = new StringBuilder();
        idQuery.append("SELECT ROWID FROM '");
        idQuery.append(mConfig.storageName);
        idQuery.append("' WHERE ");
        idQuery.append(SQLiteUtil.generateWhere(operand));

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    idQuery.toString());
        }
        SQLiteStatement idStatement = mDatabase.compileStatement(idQuery.toString());
        long rowID = idStatement.simpleQueryForLong();

        return ClassConfig.isDomesticClass(fConfig.getCollectionType()) ?
                readPrimitiveCollection(fConfig, rowID) : readForeignReferenceCollection(fConfig,
                        rowID);

    }

    @SuppressWarnings("unchecked")
    private Collection<?> readForeignReferenceCollection(FieldConfig fConfig, long rowID) {

        try {

            @SuppressWarnings("rawtypes")
            Collection result = (Collection) fConfig.field.getType().newInstance();

            StringBuilder collectionTableName = new StringBuilder();
            collectionTableName.append(mConfig.storageName);
            collectionTableName.append("_");
            collectionTableName.append(fConfig.getPersistedName());

            StringBuilder collectionQuery = new StringBuilder();
            collectionQuery.append("SELECT ");
            collectionQuery.append(fConfig.getPersistedName());
            collectionQuery.append(" FROM '");
            collectionQuery.append(collectionTableName.toString());
            collectionQuery.append("' WHERE parent_ROWID=");
            collectionQuery.append(Long.toString(rowID));

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        collectionQuery.toString());
            }
            Cursor foreignCursor = mDatabase.rawQuery(collectionQuery.toString(), null);
            int colIndex = foreignCursor.getColumnIndex(fConfig.getPersistedName());
            Class<?> castTo = fConfig.getCollectionType();

            if (foreignCursor.moveToFirst()) {
                do {
                    long foreignId = foreignCursor.getLong(colIndex);
                    Map<String, Object> criteria = new HashMap<String, Object>();
                    criteria.put("ROWID", new Long(foreignId));

                    @SuppressWarnings("rawtypes")
                    SQLiteRead<?> readOp = new SQLiteRead(mDatabase, fConfig.getCollectionType(),
                            criteria);
                    List<?> foreignObjectList = readOp.call();
                    if (foreignObjectList.size() != 1) {
                        throw new PersistException(
                                "expected to retrieve 1 reference class for collection field "
                                        + fConfig.toString() + ", but retrieved "
                                        + foreignObjectList.size());
                    }

                    result.add(castTo.cast(foreignObjectList.get(0)));

                } while (foreignCursor.moveToNext());
            }

            foreignCursor.close();

            return result;

        } catch (IllegalAccessException e) {
            throw new PersistException("unable to read collection", e);
        } catch (InstantiationException e) {
            throw new PersistException("unable to read collection", e);
        }

    }

    @SuppressWarnings("unchecked")
    private Collection<?> readPrimitiveCollection(FieldConfig fConfig, long rowID) {

        try {

            @SuppressWarnings("rawtypes")
            Collection result = (Collection) fConfig.field.getType().newInstance();

            StringBuilder collectionTableName = new StringBuilder();
            collectionTableName.append(mConfig.storageName);
            collectionTableName.append("_");
            collectionTableName.append(fConfig.getPersistedName());

            StringBuilder collectionQuery = new StringBuilder();
            collectionQuery.append("SELECT ");
            collectionQuery.append(fConfig.getPersistedName());
            collectionQuery.append(" FROM '");
            collectionQuery.append(collectionTableName.toString());
            collectionQuery.append("' WHERE parent_ROWID=");
            collectionQuery.append(Long.toString(rowID));

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        collectionQuery.toString());
            }
            Cursor primitiveCursor = mDatabase.rawQuery(collectionQuery.toString(), null);
            Class<?> castTo = fConfig.getCollectionType();

            if (primitiveCursor.moveToFirst()) {
                do {

                    int idx = primitiveCursor.getColumnIndex(fConfig.getPersistedName());
                    if (idx < 0) {
                        throw new IllegalStateException("column " + fConfig.getPersistedName()
                                + " does not exist");
                    }

                    // get the column value
                    Object value = SQLiteUtil.narrow(fConfig, primitiveCursor,
                            idx);

                    try {
                        result.add(castTo.cast(value));
                    } catch (ClassCastException cce) {
                        throw cce;
                    }
                } while (primitiveCursor.moveToNext());

                primitiveCursor.close();
            }

            return result;

        } catch (IllegalAccessException e) {
            throw new PersistException("unable to read collection", e);
        } catch (InstantiationException e) {
            throw new PersistException("unable to read collection", e);
        }

    }

    /**
     * Returns, from the operand's data stores, the values of the foreign
     * columns.
     * 
     * @return a map with foreign fields as key naems and ROWID in the foreign
     *         table as values
     * @since 1.0
     * @throws PersistException if the read fails
     * @throws SQLException if the read fails
     */
    private Map<String, Object> readForeignKeyValue(FieldConfig foreignField) {

        StringBuilder query = new StringBuilder();
        query.append("SELECT ");
        query.append(foreignField.getPersistedName());
        query.append(" FROM '");
        query.append(mConfig.storageName);
        query.append("' ");
        query.append(getWhereClause());

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    query.toString());
        }
        Cursor cursor = mDatabase.rawQuery(query.toString(), null);

        if (!cursor.moveToFirst())
            throw new PersistException("unable to retrieve foreign key values from dataabse");

        Map<String, Object> result = new HashMap<String, Object>();
        result.put("ROWID", SQLiteUtil.getColumnValue(foreignField, cursor,
                foreignField.getPersistedName()));

        cursor.close();
        return result;

    }

    /**
     * Returns the where clause needed to fetch the row identifiers from
     * operand's table.
     * 
     * @return where clause to append to the SQLite query used to fetch key
     *         values.
     */
    private String getWhereClause() {

        // fetch the key names of the foreign field config.
        Map<FieldConfig, Object> keyValues = mConfig.getKeyValues(operand);
        int sz = keyValues.size();
        int i = 0;

        StringBuilder result = new StringBuilder();
        result.append("WHERE ");

        for (Entry<FieldConfig, Object> entry : keyValues.entrySet()) {

            result.append(entry.getKey().getPersistedName());
            result.append(" = ");
            result.append(SQLiteUtil.toStringValue(entry.getValue()));
            if (i < sz - 1)
                result.append(" AND ");
            i++;

        }

        return result.toString();
    }
}
